/*
Copyright 2011 Raymond Giorgi

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */

package edu.pitt.cs3530.applicationcontrolling;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import edu.pitt.cs3530.Constants;
import edu.pitt.cs3530.SystemInfo;
import edu.pitt.cs3530.configuration.FrontEndConfiguration;
import edu.pitt.cs3530.messages.AbstractMessage;
import edu.pitt.cs3530.messages.ControlMessage;
import edu.pitt.cs3530.shareddatastructures.SocketMap;

/**
 * This class stores information about the frequencies a machine's CPU can scale
 * to as well as its current state
 */
public class Machine implements Cloneable, Serializable {

	/**
	 * Stores information on the available frequency of a particular CPU core as
	 * well as its current state
	 */
	public static class Core implements Serializable {
		private Double[] frequencies;
		private int index;
		private int cpuNo;

		private Core(ArrayList<Double> freqs, int cpuNo) {
			frequencies = new Double[freqs.size()];
			for (int i = 0; i < frequencies.length; i++) {
				frequencies[i] = freqs.get(i);
			}
			index = 0;
			this.cpuNo = cpuNo;
		}

		private double getCurrentFrequency() {
			return frequencies[index];
		}

		private double getMaxFrequency() {
			if (frequencies.length > 1) {
				return frequencies[frequencies.length - 1];
			}
			return frequencies[0];
		}

		private boolean canScaleUp() {
			return index + 1 < frequencies.length;
		}

		private boolean canScaleDown() {
			return index > 0;
		}

		private FrequencyChange differenceUp() {
			if (canScaleUp()) {
				return new FrequencyChange(frequencies[index],
						frequencies[index + 1]);
			}
			return null;
		}

		private FrequencyChange differenceDown() {
			if (canScaleDown()) {
				return new FrequencyChange(frequencies[index - 1],
						frequencies[index]);
			}
			return null;
		}

		public void scaleUp(String hostname) throws IOException {
			if (canScaleUp()) {
				index++;
				executeScale(hostname);
			}
		}

		public void scaleDown(String hostname) throws IOException {
			if (canScaleDown()) {
				index--;
				executeScale(hostname);
			}
		}

		public void scaleToMin(String hostname) throws IOException {
			index = 0;
			executeScale(hostname);
		}

		private void executeScale(String hostname) throws IOException {
			String command = "cpufreq-set -c " + cpuNo + " -f "
					+ frequencies[index] + "GHz";
			AbstractMessage message = new ControlMessage(FrontEndConfiguration
					.getInstance().getFrontEndHostName(), hostname, command);
			SocketMap.sendMessage(hostname, message);
		}
	}

	private class UpdateThread extends Thread {
		private String hostname;

		public UpdateThread(String hostname) {
			this.hostname = hostname;
		}

		@Override
		public void run() {
			try {
				Thread.sleep(5000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			updateLoadBalance(hostname);
		}
	}

	/**
	 * This method reads the available frequencies from system files and creates
	 * a datastructure to store them in
	 * 
	 * @return and object with the available CPUs and the frequencies they can
	 *         scale to
	 */
	public static Machine createFrequencies() {
		int count = 0;
		for (File f : (new File("/sys/devices/system/cpu/")).listFiles()) {
			if (f.getName().startsWith("cpu") && f.isDirectory()
					&& Character.isDigit(f.getName().charAt(3))) {
				count++;
			}
		}
		Machine freq = null;
		try {
			BufferedReader reader = null;
			StringTokenizer tok = null;
			freq = new Machine(count);
			for (int i = 0; i < count; i++) {
				reader = new BufferedReader(new FileReader(
						"/sys/devices/system/cpu/cpu" + i
								+ "/cpufreq/scaling_available_frequencies"));
				tok = new StringTokenizer(reader.readLine(), " ");
				ArrayList<Double> freqs = new ArrayList<Double>();
				while (tok.hasMoreTokens()) {
					freqs.add(0, Integer.parseInt(tok.nextToken()) / 1000000.0);
				}
				freq.addCore(new Core(freqs, i));
			}
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return freq;
	}

	/**
	 * 
	 * @return
	 */
	public static Machine Instance() {
		if (_instance == null) {
			System.err.println("Must be initialized");
		}
		return _instance;
	}

	private static Machine _instance;

	private List<Core> cores;
	private boolean active;
	private boolean toShutDown;
	private double baseLine = 1;
	private int numOfCPUs;

	private Machine(int numOfCPUs) {
		this.numOfCPUs = numOfCPUs;
		cores = new ArrayList<Core>();
		this.toShutDown = false;
	}

	private void addCore(Core core) {
		cores.add(core);
	}

	public int getNoOfCPUs() {
		return numOfCPUs;
	}

	/**
	 * Set the current state of this machine to 'shutting down'
	 * 
	 * @throws IOException
	 */
	public void shutDown(String hostname) throws IOException {
		this.toShutDown = true;
		this.offLoadBalance(hostname);
		try {
			Thread.sleep(200);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("shutting down");
		String command = "/etc/acpi/sleep.sh";
		System.out.println(command);
		AbstractMessage message = new ControlMessage(FrontEndConfiguration
				.getInstance().getFrontEndHostName(), hostname, command);
		SocketMap.sendMessage(hostname, message);
	}

	public void startUp(String macAddress) throws IOException,
			InterruptedException {
		this.toShutDown = false;
		String command = "wakeonlan " + macAddress;
		SystemInfo.executeCommand(command);
	}

	/**
	 * Returns the total power used by this machine (base line + sum)
	 * 
	 * @return the total power used by this machine (base line + sum)
	 */
	public double getTotalPower() {
		double power = this.baseLine;
		for (Core core : cores) {
			double currFreq = core.getCurrentFrequency();
			power += Math.pow(currFreq, FrontEndConfiguration.getInstance()
					.getFrequencyScaleFactor());
		}
		return power;
	}

	/**
	 * Returns the sum of all the frequencies of all machines
	 * 
	 * @return the sum of all the frequencies of all machines
	 */
	public double getTotalFrequency() {
		double total = 0;
		for (Core core : cores) {
			total += core.getCurrentFrequency();
		}
		return total;
	}

	/**
	 * Returns true if this machine is turned on
	 * 
	 * @return true if this machine is turned on
	 */
	public boolean isActive() {
		return active;
	}

	public void printLocalFrequencies(PrintStream ps) {
		String toPrint = "LocalFrequencies:" + System.currentTimeMillis()
				+ "\n";
		Iterator<Core> iter = cores.iterator();
		while (iter.hasNext()) {
			Core f = iter.next();
			toPrint += "\tCPU" + f.cpuNo + ":" + f.getCurrentFrequency();
			if (iter.hasNext()) {
				toPrint += "\n";
			}
		}
		ps.println(toPrint);
	}

	/**
	 * Returns true if one of the cores in this structure can scale up
	 * 
	 * @return true if one of the cores in this structure can scale up
	 */
	public boolean canScaleUp() {
		for (Core core : cores) {
			if (core.canScaleUp()) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns true if one of the cores in this structure can scale down
	 * 
	 * @return true if one of the cores in this structure can scale down
	 */
	public boolean canScaleDown() {
		for (Core core : cores) {
			if (core.canScaleDown()) {
				return true;
			}
		}
		return false;
	}

	public double getSmallestPowerUp() {
		double retval = Constants.DOUBLE_NULL;
		for (Core core : cores) {
			if (core.canScaleUp()) {
				FrequencyChange fd = core.differenceUp();
				if (fd.powerChange() < retval) {
					retval = fd.powerChange();
				}
			}
		}
		return retval;
	}

	public double getLargestDeltaDown() {
		double retval = Constants.DOUBLE_NEG_NULL;
		for (Core core : cores) {
			if (core.canScaleDown()) {
				FrequencyChange fd = core.differenceDown();
				if (fd.findRatio() > retval) {
					retval = fd.findRatio();
				}
			}
		}
		return retval;
	}

	// TODO: check for null
	public void scaleLargestDeltaDown(String hostname) throws IOException {
		double max = Constants.DOUBLE_NEG_NULL;
		Core coreToScale = null;
		for (Core core : cores) {
			if (core.canScaleDown()) {
				if (coreToScale == null) {
					coreToScale = core;
					max = core.differenceDown().findRatio();
				} else {
					FrequencyChange fd = core.differenceDown();
					if (fd.findRatio() > max) {
						coreToScale = core;
						max = fd.findRatio();
					}
				}
			}
		}
		if (coreToScale != null) {
			coreToScale.scaleDown(hostname);
		}
		updateLoadBalance(hostname);
	}

	public void scaleSmallestFrequencyDown(String hostname) throws IOException {
		double max = Constants.DOUBLE_NULL;
		Core coreToScale = null;
		for (Core core : cores) {
			if (core.canScaleDown()) {
				FrequencyChange fd = core.differenceDown();
				if (fd.frequencyDifference() < max
						&& fd.frequencyDifference() != 0) {
					max = fd.frequencyDifference();
					coreToScale = core;
				}
			}
		}
		if (coreToScale != null) {
			coreToScale.scaleDown(hostname);
		}
	}

	public double getFrequencyDifferenceOfLargestDelta() {
		double max = -Double.MAX_VALUE;
		double retval = 0.0;
		for (Core core : cores) {
			if (core.canScaleDown()) {
				FrequencyChange fd = core.differenceDown();
				if (fd.findRatio() > retval) {
					max = fd.findRatio();
					retval = fd.frequencyDifference();
				}
			}
		}
		return retval;
	}

	public double getLargestFrequency() {
		double retval = -Double.MAX_VALUE;
		for (Core core : cores) {
			if (core.canScaleDown()) {
				if (retval < core.getCurrentFrequency()) {
					retval = core.getCurrentFrequency();
				}
			}
		}
		return retval;
	}

	public double getSmallestFrequencyDown() {
		double retval = Double.MAX_VALUE;
		for (Core core : cores) {
			if (core.canScaleDown()) {
				FrequencyChange fd = core.differenceDown();
				if (fd.frequencyDifference() < retval) {
					retval = fd.frequencyDifference();
				}
			}
		}
		return retval;
	}

	public void scaleUpSmallest(String hostname) throws IOException {
		Core min = null;
		double minValue = Constants.DOUBLE_NULL;
		for (Core core : cores) {
			if (core.canScaleUp()) {
				FrequencyChange fd = core.differenceUp();
				if (fd.powerChange() < minValue) {
					minValue = fd.powerChange();
					min = core;
				}
			}
		}
		if (min != null) {
			min.scaleUp(hostname);
		}
		updateLoadBalance(hostname);
	}

	public double getSmallestDeltaUp() {
		double retval = Constants.DOUBLE_NULL;
		for (Core core : cores) {
			if (core.canScaleUp()) {
				FrequencyChange fd = core.differenceUp();
				if (fd.findRatio() < retval) {
					retval = fd.findRatio();
				}
			}
		}
		return retval;
	}

	public double getMaxTotalFrequency() {
		double total = 0.0;
		for (Core core : cores) {
			total += core.getMaxFrequency();
		}
		return total;
	}

	public void scaleToMin(String hostname, boolean delay) throws IOException {
		for (Core core : cores) {
			core.scaleToMin(hostname);
		}
		if (!delay) {
			updateLoadBalance(hostname);
		} else {
			new UpdateThread(hostname).start();
		}
	}

	public void updateLoadBalance(String hostname) {
		int lbfactor = (int) (this.getTotalFrequency() * 10 + .5);
		String filename = this.getFilename(hostname);
		writeLoadBalance(filename, "" + lbfactor);
	}

	public void offLoadBalance(String hostname) {
		String filename = this.getFilename(hostname);
		String message = "" + 0;
		writeLoadBalance(filename, message);
	}

	private void writeLoadBalance(String filename, String message) {
		try {
			BufferedWriter writer = new BufferedWriter(new FileWriter(filename));
			writer.write(message);
			writer.flush();
			writer.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println(filename + ":::" + message);
	}

	private String getFilename(String hostname) {
		String retval = FrontEndConfiguration.getInstance()
				.getLoadBalancerFactoryDirectory()
				+ hostname;
		return retval;
	}
}
